﻿using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Diagnostics.Contracts;

using ReactiveLang;

using E = System.Linq.Expressions.Expression;
using RE = ReactiveLang.Expression;

namespace ReactiveLang.ExpressionConverter
{
    using ReactiveLang.Util;
    using Util;

    static class Extensions
    {
        static Dictionary<int, Type> _tupleTypes = typeof(Tuple).Assembly.GetTypes()
            .Where(t => t.FullName.StartsWith("System.Tuple`"))
            .ToDictionary(t => t.GetGenericArguments().Length);

        static Type GetTupleType(params Type[] itemTypes)
        {
            return _tupleTypes[itemTypes.Length].MakeGenericType(itemTypes);
        }
        static E TupleCreate(params E[] items)
        {
            var itemTypes = items.Select(i => i.Type).ToArray();
            return E.New(_tupleTypes[items.Length].MakeGenericType(itemTypes).GetConstructor(itemTypes), items);
        }
        public static LambdaExpression CreateTuple(Type[] paramTypes, Type[] itemTypes)
        {
            var parameters = paramTypes.Select(t => E.Parameter(t)).ToArray();
            var items = parameters.Select((p, i) => p.Type == itemTypes[i] ? p as E : E.Convert(p, itemTypes[i])).ToArray();
            return E.Lambda(TupleCreate(items), parameters);
        }
        public static LambdaExpression CreateTuple(params Type[] paramTypes)
        {
            return CreateTuple(paramTypes, paramTypes);
        }
        static E AddItemToTuple(E tuple, E item)
        {
            var items = new List<E>();
            if (tuple.Type.Assembly == typeof(Tuple).Assembly && tuple.Type.FullName.StartsWith(typeof(Tuple).FullName))
            {
                int itemCount = tuple.Type.GetGenericArguments().Length;

                // todo: max 7 item support
                items.AddRange(Enumerable.Range(1, itemCount).Select(i => E.Property(tuple, "Item" + i) as E));
            }
            else
                items.Add(tuple);

            items.Add(item);
            return TupleCreate(items.ToArray());

        }

        [Pure]
        public static Type GetElementType(this E expressionOrParent)
        {
            Contract.Requires(expressionOrParent != null);
            return (from iface in Enumerable.Repeat(expressionOrParent.Type, 1).Concat(expressionOrParent.Type.GetInterfaces())
                    where iface.IsGenericType && (iface.GetGenericTypeDefinition() == typeof(IExpression<>) || iface.GetGenericTypeDefinition() == typeof(IExpressionParent<>))
                    select iface.GetGenericArguments().Single())
                    .Single();
        }


        static E Op(string name, Type[] typeArgs, params E[] args)
        {
            Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(name));

            var methods = typeof(RE).GetMember(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).OfType<MethodInfo>();

            if (typeArgs != null && typeArgs.Any())
                methods = from m in methods
                          where m.IsGenericMethodDefinition
                          let genericParams = m.GetGenericArguments()
                          where genericParams.Length == typeArgs.Length && genericParams.Zip(typeArgs, (p, a) => p.BaseType.IsAssignableFrom(a)).All(x => x)
                          select m.MakeGenericMethod(typeArgs);

            methods = from m in methods
                      let parameters = m.GetParameters()
                      where parameters.Length == args.Length
                            && parameters.Zip(args, (p, a) => p.ParameterType.IsAssignableFrom(a.Type)).All(x => x)
                      select m;

            Contract.Assert(methods.Count() == 1);
            return E.Call(methods.Single(), args);
        }
        static IList<E> ExtractOp(this E op, Type containerType, string methodName)
        {
            var call = op as MethodCallExpression;
            if (call == null)
                return null;
            if (call.Method.Name != methodName || call.Method.DeclaringType != containerType || !call.Method.IsStatic)
                return null;
            return call.Arguments;
        }
        static IList<E> ExtractOp(this E op, string methodName)
        {
            return op.ExtractOp(typeof(RE), methodName);
        }


        public static E Binary(this E left, E right, LambdaExpression resultSelector)
        {
            return Binary(left, right, resultSelector, resultSelector.Body.Type);
        }
        public static E Binary(this E left, E right, E resultSelector, Type resultType)
        {
            return Op("Binary", new[] { left.GetElementType(), right.GetElementType(), resultType }, left, right, resultSelector);
        }
        public static E Binary(this E left, E right, bool mergeTuples = false)
        {
            if (left == null)
            {
                left = right;
                right = null;
            }

            if (right == null)
            {
                //if (!mergeTuples)
                    return left;
                
                //var p = E.Parameter(left.GetElementType(), "x");
                //var tupleSelector = E.Lambda(
                //    E.New(typeof(Tuple<>).MakeGenericType(p.Type).GetConstructors().Single(), p),
                //    p
                //    );
                //return left.Select(tupleSelector);
            }

            var item1 = E.Parameter(left.GetElementType());
            var item2 = E.Parameter(right.GetElementType());
            LambdaExpression selector = mergeTuples ? E.Lambda(AddItemToTuple(item1, item2), item1, item2) : CreateTuple(item1.Type, item2.Type);
            return Binary(left, right, selector);
        }

        public static E Binary(this IEnumerable<E> sources, Func<E[], E> resultSelector = null, bool useApply = false)
        {
            Contract.Requires(sources != null && sources.Any() && !sources.Contains(null));
            Contract.Ensures(Contract.Result<E>() != null);
            sources = sources.ToList();

            //if (sources.Count() == 1 && resultSelector != null)
            //{
            //    var src = sources.Single();
            //    var item = E.Parameter(src.GetElementType());
            //    return sources.Single().Select(E.Lambda(resultSelector(new []{item}), item));
            //}

            E merged = sources.Aggregate<E, E>(null, (a, b) => Binary(a, b, true));
            Contract.Assert(merged != null);

            if (resultSelector == null)
                return merged;

            var p = E.Parameter(merged.GetElementType());
            var items = Enumerable.Range(1, sources.Count()).Select(i => E.Property(p, "Item" + i) as E).ToArray();

            var result = E.Lambda(resultSelector(items), p);
            return useApply ? merged.Apply(result) : merged.Unary(result);
        }
        public static E CombineLatestToArray(this IEnumerable<E> sources, Type elemType)
        {
            Contract.Assert(sources != null && sources.All(s => s.GetElementType() == elemType));

            var sourceArray = E.NewArrayInit(typeof(IObservable<>).MakeGenericType(elemType), sources);
            return Op("CombineLatest", new[] { elemType }, sourceArray);
        }

        public static E Unary(this E source, LambdaExpression resultSelector)
        {
            Contract.Requires(source != null);
            Contract.Requires(resultSelector != null && resultSelector.Parameters.Count == 1);

            E returnValue;
            int paramUsageCount = resultSelector.Body.Expand().Count(x => x == resultSelector.Parameters[0]);
            if (source.ExtractConstant(out returnValue))
            {
                if (paramUsageCount == 1)
                    return Constant(resultSelector.Body.Replace(resultSelector.Parameters[0], returnValue));

                var src = E.Variable(returnValue.Type);
                return Constant(E.Block(
                    new[] { src },
                    E.Assign(src, returnValue),
                    resultSelector.Body.Replace(resultSelector.Parameters[0], src)
                ));
            }

            E selectSource;
            LambdaExpression baseSelector;
            if (source.ExtractUnary(out selectSource, out baseSelector))
            {
                var baseParam = baseSelector.Parameters[0];
                E newBody;
                if (paramUsageCount == 1)
                {
                    newBody = resultSelector.Body.Replace(resultSelector.Parameters[0], baseSelector.Body);
                }
                else
                {
                    var src = E.Variable(baseSelector.Body.Type);
                    newBody = E.Block(
                        new[] {src}, 
                        E.Assign(src, baseSelector.Body),
                        resultSelector.Body.Replace(resultSelector.Parameters[0], src)
                    );
                }
                resultSelector = E.Lambda(
                    newBody,
                    baseParam
                );
                return selectSource.Unary(resultSelector);
            }

            return Op("Unary", new[] { source.GetElementType(), resultSelector.Body.Type }, source, resultSelector);
        }
        public static bool ExtractUnary(this E select, out E source, out LambdaExpression resultSelector)
        {
            source = null;
            resultSelector = null;
            var args = select.ExtractOp("Unary");
            if (args == null)
                return false;

            source = args[0];
            resultSelector = (LambdaExpression)args[1];
            return true;
        }
        public static E Where(this E source, LambdaExpression predicate)
        {
            return Op("Where", new[] { source.GetElementType() }, source, predicate);
        }

        public static E Apply(this E source, LambdaExpression resultSelector)
        {
            Contract.Requires(source != null && resultSelector != null);

            E value;
            if (source.ExtractConstant(out value))
                return resultSelector.Body.Replace(resultSelector.Parameters[0], value);

            return Op("Apply", new[] { source.GetElementType(), resultSelector.Body.GetElementType() }, source, resultSelector);
        }
        public static E Distinct(this E source)
        {
            return Op("Distinct", new[] { source.GetElementType() }, source);
        }
        public static E Constant(E value)
        {
            return Op("Constant", new[] { value.Type }, value);
        }
        public static bool ExtractConstant(this E @return, out E value)
        {
            value = null;
            var args = @return.ExtractOp("Constant");
            if (args == null)
                return false;
            value = args[0];
            return true;
        }

        public static E If(this E test, E ifTrue, E ifFalse)
        {
            Contract.Assert(test != null && ifTrue != null && ifFalse != null);
            Contract.Assert(test.GetElementType() == typeof(bool) && ifTrue.GetElementType() == ifFalse.GetElementType());
            return Op("If", new[] { ifTrue.GetElementType() }, test, ifTrue, ifFalse);
        }
        public static E Cast(this E source, Type resultType)
        {
            Contract.Requires(source != null);
            Contract.Ensures(Contract.Result<E>() != null && Contract.Result<E>().GetElementType() == resultType);
            if (source.GetElementType() == resultType)
                return source;
            var item = E.Parameter(source.GetElementType(), "x");
            var lambda = E.Lambda(E.Convert(item, resultType), item);
            return source.Unary(lambda);
        }
        public static E AsUnit(this E source)
        {
            Contract.Requires(source != null);
            Contract.Ensures(Contract.Result<E>() != null && Contract.Result<E>().GetElementType() == typeof(Unit));
            return Op("AsUnit", new[] { source.GetElementType() }, source);
        }
        public static E WithInitial(this E unitSource)
        {
            Contract.Requires(unitSource != null && unitSource.GetElementType() == typeof(Unit));
            Contract.Ensures(Contract.Result<E>() != null);
            return Op("WithInitial", Type.EmptyTypes, unitSource);
        }

        public static E Event(E target, E @event, Type eventArgsType)
        {
            Contract.Requires(@event != null && typeof(EventInfo).IsAssignableFrom(@event.Type));
            return Op("Event", new Type[] {eventArgsType}, target, @event);
        }
        internal static E Event(E target, EventInfo @event)
        {
            Contract.Requires(@event != null);
            Contract.Requires(@event.EventHandlerType.GetMethod("Invoke").GetParameters().Length == 2
                && @event.EventHandlerType.GetMethod("Invoke").GetParameters()[0].ParameterType == typeof(object)
                );
            Type eventArgsType = @event.EventHandlerType.GetMethod("Invoke").GetParameters()[1].ParameterType;
            return Event(target, E.Constant(@event), eventArgsType);
        }

        public static E LatestContinuous(this E source)
        {
            var result = Op("Latest", new[] {source.GetElementType()}, source, ExpressionUtil.True);
            Contract.Assert(result.Type.IsGenericType && result.Type.GetGenericTypeDefinition() == typeof(Expression.LatestValue<>));
            return result;
        }

        public static E ObserveOn(this E source, E scheduler)
        {
            return Op("ObserveOn", new[] { source.GetElementType() }, source, scheduler);
        }

        public static E Timer(E dueTime, E interval)
        {
            Contract.Requires(dueTime != null && dueTime.Type == typeof(TimeSpan));
            Contract.Requires(interval != null && interval.Type == typeof(TimeSpan));
            return Op("Timer", Type.EmptyTypes, dueTime, interval);
        }

        public static E Box(this E expression)
        {
            return Op("Box", new[] { expression.GetElementType() }, expression);
        }
    }
}
